KGme by Neitsa[FRET]
Codé par Neitsa
Cracké par ++Meat
le 16/12/05

Niveau simple


 

 

01 - PLAN DU TUTORIEL
=====================

01 - PLAN DU TUTORIEL

02 - INCIPIT

03 - .NET C'EST QUOI CA ENCORE ?
   03.00 - C'EST DIFFICILE LE CRACKING .NET ?

04 - KEYGENING
   04.00 - SHAKE IT BABY
   04.01 - OPOPOP... D'ACCORD MAIS POURQUOI ?
   04.02 - ANALYSE DE LA METHODE
   04.03 - CODING DU KEYGEN

05 - PATCHING
   05.00 - PATCHING BRUT
      05.00.01 - LE CODE A MODIFIER
      05.00.02 - OU MODIFIER LE CODE ?
      05.00.03 - J'AI RIEN COMPRIS ! ON RECOMMENCE
   05.01 - RECOMPILATION

06 - HAPPY END
   06.00 - PUSH REALLIFE/RET

 

 

02 - INCIPIT
============

Aye aye !

Aujourd'hui nous allons nous attaquer à un petit keygeneme tout simple mais qui à la particularité d'être un crackme .NET (C# ou VB.NET on ne peut pas savoir) d'ailleurs voici ce que nous dit PEiD :

"Microsoft Visual C# / Basic .NET"

Le .NET est quelque chose d'assez nouveau et assez flou (pour certains) etant donné l'abus d'acronymes utilisé... Cependant, cela fait parti des choses qu'il faudra tenter de maitriser à l'avenir.
Je ne vous cache pas que je suis un neophyte dans ce domaine. Ce tuto peut donc contenir son lot de conneries qui fera sans doute grincer les dents des plus experimentés... Veuillez m'en excuser.

Bref, nous verrons tout cela plus tard.

Voici ce que nous dit l'ami Neitsa en attendant :) (cf. ForumCrack)

Youp !

Un petit Keygenme (avec patching accepté pour ceux ne souhaitant pas faire un Keygen).

La difficulté dépend des moyens que vous utilisez pour l'attaque... La routine est très très simple, le problème sera sûrement de savoir où elle se trouve :D

Un petit tut serait pas mal... ;)

Merci à vous. Je releaserais un petit tut de mon coté. Bon courage.

(...)

Nous allons donc faire le patch ET le keygen (oui je m'ennuie pas mal ces temps-ci, j'avoue).

Et n'oubliez pas : Ce tutoriel à beau être long, il n'y à rien de vraiment compliqué !

Allez... Fait peter Simone !

 

 

03 - .NET C'EST QUOI CA ENCORE ?
================================

Bonne question, je vais tenter d'y repondre clairement et simplement. Pas évident dans la mesure ou j'entrave pas au chose au charabia qu'on peut trouver sur le net.
Ceci est loin d'être une reference, c'est néanmoins le strict minimum pour comprendre le fonctionnement de ce crackme.

Bon... En fait il y a le Win32 et le .NET.

Le Win32 est composé d'un ensemble de librairies (user32.dll, kernel32.dll, etc) aussi apellées API. Dans ces APIs sont stockées des fonctions permettant au programmeur d'accomplir certaines taches sans avoir à tout coder lui même.
Avec ces fonctions nous pouvons par exemple afficher une message box (fonction MessageBox dans user32.dll), ouvrir un fichier (OpenFile dans kernel32.dll), etc, etc...

Le programmeur choisi le langage qu'il souhaite pour produire du code Win32 (Assembleur, C, C++ etc...). Le code produit par le programmeur est en suite traduit en langage machine par le compilateur, et est donc comprehensible par le micro-processeur.

Ca devrait être un fait acquis pour vous, ou bien vous avez encore besoin de potasser un peu sur le sujet...

Qu'en est-il du .NET maintenant ?

C'est un chouia plus complexe. En fait nous avons le .NET framework qui fait office d'API ici, mais sous forme d'objets. Je m'explique : Le .NET framework est une grande librairie (mscoree.dll) contenant plusieurs namespaces, classes, méthodes etc...

Prenons par exemple le cas de l'affichage d'une message box en C :

MessageBox(hWnd, "Salut", "Wesh", MB_OK);

Cette ligne de code fait appel à la fonction MessageBox contenue dans la librairie user32.dll.

Voici le même exemple en C# :

System.Windows.Forms.MessageBox.Show("Salut", "Wesh", MessageBoxButtons.OK);

(Il existe un moyen de simplifier la lisibilité du code mais cela depasse du cadre de ce tutoriel).

Voici donc ce que nous avons :

System.Windows.Forms.MessageBox.Show
\__________________/ \________/ \__/
 |                    |          |
 |                    |          |
 |                    |          +- La méthode
 |                    |
 |                    +- La classe
 |
 +- Le namespace

Le namespace contient plusieurs classes. Les classes contiennent plusieurs méthodes (fonctions), et un tas d'autre trucs passionants.

Vous avez maintenant une breve idée du fonctionnement du .NET framework.

Alors qu'en Win32 le code produit par le programmeur était directement traduit en langage machine (à peu de choses près), en .NET c'est different. Le code est traduit en IL.

IL pour Intermediate Language (ou MSIL pour Microsoft Intermediate Language) est un langage intermediaire (comme le Port Salut). C'est l'equivalent du langage machine pour les programmes .NET. Ce code est traduit à la volée en langage machine lors de l'execution du programme.

N'esperez donc pas trouver d'instructions Asm avec votre desassembleur/debuggeur préféré pauvres mortels. (Sauf peut-être avec IDA PRO 4.8, mais chez moi il reste muet...)

J'espère avoir été assez clair la dessus, et pas avoir raconté trop de conneries. J'espère aussi que ma comparaison avec le Win32 est judicieuse... Qui-sait, peut-être que dans quelques années les gens se diront "C'est quoi c'truc, Win32 ?".

03.00 - C'EST DIFFICILE LE CRACKING .NET ?
------------------------------------------

On va voir ça tout de suite :)

 

 

04 - KEYGENING
==============

Commencons par le plus simple, le keygen (bien que ça puisse sembler paradoxal)...

Nous allons pour ça nous armer d'un tool tres util, il s'agit de Reflector (cf. Liens du site de la NAS, section Outils).

Reflector est un decompilateur pour les programmes .NET. Cela signifie qu'il sera apte à nous fournir les sources d'un programme .NET, que ce soit en IL, C#, VB .NET ou Delphi .NET. A l'heure actuelle (4.1.95.0) il est encore un peu buggé, mais nous nous en contenterons...

04.00 - SHAKE IT BABY
---------------------

Nous allons donc ici, à l'aide de notre décompilateur, acceder aux différentes méthodes du programme (en .NET tout est objetisé, les programmes, les variables, bref... Tout !), pour pouvoir analyser celles-ci. Nous coderons ensuite notre keygens à l'aide des informations obtenues.

On lance donc Reflector, une dialgbox peut apparaitre nous demandant la version du .NET frameword à utiliser, choisisons donc la plus recente, et c'est parti !

Le programme va donc charger les composants du framework. Une fois que c'est fait, chargez le crackme dans Reflector.

Un nouvel item 'WindowsApplication1' devrait apparaitre. Developpez le sous item 'WindowsApplication1.exe' (le module), puis le sous item 'WindowsApplication1' (le namespace), et enfin 'Form1' (la classe). Parfait... Nous avons maintenant acces aux differentes méthodes contenues dans le programme, pas trop fatigué ?

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- WindowsApplication1 (assembly)
  |
  - WindowsApplication1.exe (module)
    |
    - WindowsApplication1 (namespace)
      |
      - Form1 (classe)
        |
        + button1_Click(Object, EventArgs) : Void
        + Main() : Void
        + Rev(String) : String
        + D'autres méthodes...

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Les méthodes sont affichés de la manière suivante : NomDeLaMethode(Argument1, Argument2, ArgumentN) : TypeDeValeurDeSortie

J'ai pas fait de grandes études mais je suppute que c'est la méthode 'button1_Click' qui nous interesse. Et effectivement c'est le cas.

04.01 - OPOPOP... D'ACCORD MAIS POURQUOI ?
------------------------------------------

Pour nous en convaincre il suffit de desassembler la méthode InitializeComponent() et de jetter un oeil à cet endroit :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      this.button1.Location = new Point(0x6d, 0x80);
      this.button1.Name = "button1";
      this.button1.TabIndex = 1;
      this.button1.Text = "&Verify";
      this.button1.Click += new EventHandler(this.button1_Click);

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Voici les propriétés de l'objet button1. La méthode 'Click' de l'objet est associée à 'EventHandler(this.button1_Click)'.

En gros, quand toi y en a cliquer sur le bouton, le programme y en a appeler la méthode 'button1_Click'...

04.02 - ANALYSE DE LA METHODE
-----------------------------

Voici le code obtenu (en C#) de la méthode 'button1_Click'. Vous ne rêvez pas... Vous êtes bien en train de cracker le programme :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

private void button1_Click(object sender, EventArgs e)
{
      int num1 = 0;
      string text1 = this.textBox1_Name.Text;
      if (text1.Length < 5)                                // Si nom < 5, on dégage.
      {
            MessageBox.Show("Name too short");
      }
      else if (this.textBox2_Pass.Text.Length < 5)         // Si serial < 5, on dégage
      {
            MessageBox.Show("Pass too short");
      }
      else
      {
            string text4 = text1;                          // text4 contient notre nom.
            for (int num3 = 0; num3 < text4.Length; num3++)
            {
                  char ch1 = text4[num3];
                  int num2 = ch1;
                  num1 += num2;                            // On additionne tous les chars du nom dans num1.
            }
            string text2 = text1 + num1;                   // Concatenation du nom avec num1 dans text2.
            string text3 = this.Rev(text2);                // on appelle la méthode Rev pour tranformer text2 dans text3.
            if (text3 != this.textBox2_Pass.Text)          // text3 est-il different du serial entré ?
            {
                  MessageBox.Show("BadBoy !");             // Ben oui... Donc on dégage.
            }
            else
            {
                  MessageBox.Show("GoodBoy !!!");          // Non... Donc on a gagné !
            }
      }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Le code se devine facilement pour peu que vous ayez quelques bases de programmation C (ou C# au mieux).
Si par exemple vous êtes un habitué du Delphi ou Visual Basic, une petite list box vous permet de choisir le langage generé. Elle est pas belle la vie ? Si, hein...

On pourait très bien faire un copier/coller de la routine et la debugger avec le debugger integré à MSVC#... Et c'est ce que nous allons faire pour la routine suivante... Ceci nous permettra de mieux appréhender le code.

Etudions donc la méthode Rev, en cliquant dessus on voit aparaitre son code :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

private string Rev(string str)
{
      char[] chArray1 = str.ToCharArray();            // On tranforme la string en tableau de char.
      int num1 = str.Length - 1;                      // num1 vaut la longeur de la string moins le NULL Terminating Char.
      if (1 == num1)                                  // Si c'est egal à 1,
      {
            return str;                               // On retourne la string d'entrée.
      }
      int num2 = 0;
      while (num2 < num1)
      {
            char[] chArray2;
            IntPtr ptr1;
            (chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num2)] = chArray2[(int) ptr1] ^ chArray1[num1];
            (chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num1)] = chArray2[(int) ptr1] ^ chArray1[num2];
            (chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num2)] = chArray2[(int) ptr1] ^ chArray1[num1];
            num2++;
            num1--;
      }
      return new string(chArray1);
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

On crée donc un nouveau projet (Console Application) avec MSVC# et on colle la routine Rev de manière à avoir le code suivant.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

using System;
using System.Collections.Generic;
using System.Text;

namespace test
{
    class Program
    {
        static string Rev(string str)
        {
            char[] chArray1 = str.ToCharArray();
            int num1 = str.Length - 1;
            if (1 == num1)
            {
                return str;
            }
            int num2 = 0;
            while (num2 < num1)
            {
                char[] chArray2;
                IntPtr ptr1;
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] = (char)(chArray2[(int)ptr1] ^ chArray1[num1]);
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num1)] = (char)(chArray2[(int)ptr1] ^ chArray1[num2]);
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] = (char)(chArray2[(int)ptr1] ^ chArray1[num1]);
                num2++;
                num1--;
            }
            return new string(chArray1);
        }

        static void Main(string[] args)
        {
            Console.WriteLine(Rev("meatreya"));
        }
    }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Notez que j'ai corrigé le casting qui generé une erreur dans la méthode 'Rev' : '(char)(chArray2[(int)ptr1] ^ chArray1[num1]);'

J'appelle ensuite ma méthode 'Rev' en lui donnant comme argument une string. J'affiche ensuite la string de sortie avec la méthode 'WriteLine'.
Nous savons ceci grace à la signature de la méthode : 'Rev(String) : String'

L'appel à 'Rev' se fait dans la méthode 'Main' de la classe 'Program', il s'agit de l'entry point du program. C'est à dire la méthode qui sera executée directement après le lancement du programme.

Vous pouvez debugger et vous apercevoir que ce que fait la méthode 'Rev' est une inversion de l'ordre des chars de la chaine fournie.

04.03 - CODING DU KEYGEN
------------------------

Pour coder ce keygen, rien de plus simple, je vais vous montrer la marche à suivre.
Tout d'abord voyons ensemble le schéma du fonctionement du crackme :

Le Crackme :

- Si la longeur du nom < 5 : Nom invalide.
- Si la longeur du serial < 5 : Serial invalide.
- On calcul la somme de tous les chars du nom.
- On concatène la somme obtenue à la suite du nom ("JeanMichel123").
- On inverse l'ordre des charactères de la string "JeanMichel123".
- Si la string inversée correspond au serial entré par l'utilisateur : C'est bon.
- Sinon : C'est pas bon.

C'est relativement simple. Maintenant voyons le schéma du fonctionnement du keygen que nous devrons coder :

Le Keygen :

- Si la longeur du nom < 5 : Nom invalide.
- On calcul la somme de tous les chars du nom.
- On concatène la somme obtenue à la suite du nom ("JeanMichel123").
- On inverse l'ordre des charactères de la string "JeanMichel123".
- La string inversée est notre serial.

Le code source de mon keygen est le suivant (je reprend mon projet console precedent) :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

using System;
using System.Collections.Generic;
using System.Text;

namespace test
{
    class Program
    {
        static string Rev(string str)
        {
            char[] chArray1 = str.ToCharArray();
            int num1 = str.Length - 1;
            if (1 == num1)
            {
                return str;
            }
            int num2 = 0;
            while (num2 < num1)
            {
                char[] chArray2;
                IntPtr ptr1;
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] = (char)(chArray2[(int)ptr1] ^ chArray1[num1]);
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num1)] = (char)(chArray2[(int)ptr1] ^ chArray1[num2]);
                (chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] = (char)(chArray2[(int)ptr1] ^ chArray1[num1]);
                num2++;
                num1--;
            }
            return new string(chArray1);
        }

        static void Main(string[] args)
        {
            int a = 0;

            Console.Write("KGme by Neitsa[FRET]\nHaXxOr3d by ++Meat\n\nEntrez votre nom : ");

            String Nom = Console.ReadLine();
            if (Nom.Length < 5)
            {
                Console.WriteLine("Le nom est trop court...\n");
                return;
            }

            for (int i = 0; i < Nom.Length; i++)
            {
                a += (int)Nom[i];
            }

            String Serial = Nom + a;
            Console.WriteLine("Le serial est : " + Rev(Serial) + "\n\nA bientot...\n");
        }
    }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Je pense que cela se passe de commentaires. Si vous rencontrez quelques difficultés, renseignez vous sur le langage C#. Soit via des ouvrages specialisés, soit via des documentations trouvées sur le net.

L'execution du keygen nous donne ceci :

KGme by Neitsa[FRET]
HaXxOr3d by ++Meat

Entrez votre nom : meatreya
Le serial est : 658ayertaem

A bientot...

Appuyez sur une touche pour continuer...

On test, et ça fonctionne. Qui en aurait douté ?

Passons maintenant au patching du programme. Nous verrons deux façons différentes de faire cela.

 

 

05 - PATCHING
=============

Le patching de ce keygeneme consistera à reverser un saut conditionnel. Rien de bien excitant me diriez-vous. Pourtant ce nouveau type de cible nous amène à de nouvelles approches pour parvenir à nos fins.
Comme je l'ai dit plus haut, nous allons proceder de deux manières différentes. Commençons par la première...

05.00 - PATCHING BRUT
---------------------

Ici nous allons voir quel est le code à reverser, ensuite nous le repèrerons dans le fichier avec un éditeur héxadécimal classique, puis nous remplacerons la ou les opcodes necessaires en héxadécimal. Nous verrons les difficultés recontrées au fur et à mesure...

Revoyons la méthode contenant la verification GoToGoodBoy/GoToBadBoy, il s'agit de la méthode 'button1_Click'. Reperons ensuite la-dite vérification :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

private void button1_Click(object sender, EventArgs e)
{
      int num1 = 0;
      string text1 = this.textBox1_Name.Text;
      if (text1.Length < 5)                                // Si nom < 5, on dégage.
      {
            MessageBox.Show("Name too short");
      }
      else if (this.textBox2_Pass.Text.Length < 5)         // Si serial < 5, on dégage
      {
            MessageBox.Show("Pass too short");
      }
      else
      {
            string text4 = text1;                          // text4 contient notre nom.
            for (int num3 = 0; num3 < text4.Length; num3++)
            {
                  char ch1 = text4[num3];
                  int num2 = ch1;
                  num1 += num2;                            // On additionne tous les chars du nom dans num1.
            }
            string text2 = text1 + num1;                   // Concatenation du nom avec num1 dans text2.
            string text3 = this.Rev(text2);                // on appelle la méthode Rev pour tranformer text2 dans text3.
            if (text3 != this.textBox2_Pass.Text)          // text3 est-il different du serial entré ? <-- Hep ! Psst ! Petite fille !
            {
                  MessageBox.Show("BadBoy !");             // Ben oui... Donc on dégage.
            }
            else
            {
                  MessageBox.Show("GoodBoy !!!");          // Non... Donc on a gagné !
            }
      }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

La ligne interessante est :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

            if (text3 != this.textBox2_Pass.Text)          // text3 est-il different du serial entré ? <-- Hep ! Psst ! Petite fille !

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Il suffirait de changer le '!=' en '==' pour valider le serial dans le cas où celui-ci serait incorrect (le B-A BA du cracking, hu !).

Tout va bien, mais nous avons besoin d'informations supplémentaires pour mener à bien notre mission. Nous devons savoir où se situe le code en question, et... Quel est le code en question ? Etant donné que nous ne pouvons modifier le code recompilé, nous devrons jouer avec le IL.

05.00.01 - LE CODE A MODIFIER
-----------------------------

Très bien, il y à une listbox en haut de la fenêtre avec la liste des langages disponibles, choisissons le langage "IL" et regardons la tournure que notre listing a pris :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

(...)
      L_0086: ldfld [System.Windows.Forms]System.Windows.Forms.TextBox WindowsApplication1.Form1::textBox2_Pass
      L_008b: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()
      L_0090: call bool string::op_Inequality(string, string)
      L_0095: brfalse.s L_00a3
      L_0097: ldstr "BadBoy !"
      L_009c: call [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
      L_00a1: pop
      L_00a2: ret
      L_00a3: ldstr "GoodBoy !!!"
      L_00a8: call [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
      L_00ad: pop
      L_00ae: ret
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Même si je ne connais pas le langage IL, ma cervelle me permet de savoir que ce code correspond grossierement à :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

(...)
            if (text3 != this.textBox2_Pass.Text)         // text3 est-il different du serial entré ? <-- Hep ! Psst ! Petite fille !
            {
                  MessageBox.Show("BadBoy !");            // Ben oui... Donc on dégage.
            }
            else
            {
                  MessageBox.Show("GoodBoy !!!");         // Non... Donc on a gagné !
            }
      }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Donc à notre verification... Interessons-nous à ces quelques lignes :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      L_0090: call bool string::op_Inequality(string, string)
      L_0095: brfalse.s L_00a3
      L_0097: ldstr "BadBoy !"
      L_009c: call [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
      L_00a1: pop
      L_00a2: ret
      L_00a3: ldstr "GoodBoy !!!"
      L_00a8: call [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
      L_00ad: pop
      L_00ae: ret

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Toujours avec notre cervelle, on peut traduire ce listing en pseudo-code :

- Verification de l'inegalité des strings (renvoi 'true' = strings differentes / renvoi 'false' = strings egales)
- Si false, on va à L_00A3 (good boy)

- Affiche "BadBoy !"
  - Retour

L_00A3:

- Affiche "GoodBoy !!!"
  - Retour

Remplacons donc le 'Si pas egal' (brfalse.s) en 'Si egal' (on suppose qu'il s'agit de 'brtrue.s'). Pour plus d'information, nous devons telecharger la documentation sur le IL sur ce site : "http://msdn.microsoft.com/net/ecma/default.asp"
Il s'agit du fichier : "ECMA-335: CLI Partition III - CIL (word/pdf zip)"

Faisons parler ce document :

brfalse.<length> - branch on false, null, or zero

+-----------+------------------+---------------------------------------------------------------------+
| Format    | Assembly Format  | Description                                                         |
+-----------+------------------+---------------------------------------------------------------------+
| 39 <I4>   | brfalse target   | branch to target if value is zero (false)                           |
| 2C <I1>   | brfalse.s target | branch to target if value is zero (false), short form               |
| 39 <I4>   | brnull target    | branch to target if value is null (alias for brfalse)               |
| 2C <I1>   | brnull.s target  | branch to target if value is null (alias for brfalse.s), short form |
| 39 <I4>   | brzero target    | branch to target if value is zero (alias for brfalse)               |
| 2C <I1>   | brzero.s target  | branch to target if value is zero (alias for brfalse.s), short form |
+-----------+------------------+---------------------------------------------------------------------+

brtrue.<length> - branch on non-false or non-null

+-----------+------------------+-------------------------------------------------------------------------------------------+
| Format    | Assembly Format  | Description                                                                               |
+-----------+------------------+-------------------------------------------------------------------------------------------+
| 3A <I4>   | brtrue target    | branch to target if value is non-zero (true)                                              |
| 2D <I1>   | brtrue.s target  | branch to target if value is non-zero (true), short form                                  |
| 3A <I4>   | brinst target    | branch to target if value is a non-null object reference (alias for brtrue)               |
| 2D <I1>   | brinst.s target  | branch to target if value is a non-null object reference, short form (alias for brtrue.s) |
+-----------+------------------+-------------------------------------------------------------------------------------------+

L'instruction 'brfalse' effectue un branchement (un saut) à l'offset '$+<lenght>' (où $ est l'offset actuelle), si la valeur 'false'.
L'instruction 'brtrue' fait l'inverse, branchement si la valeur vaut 'true'.

Pour connaitre l'opcode que nous devrons patcher il faut trouver le format de l'instruction 'brtrue' correspondant au format de notre 'brfalse'. Donc :

+-----------+------------------+---------------------------------------------------------------------+
| Format    | Assembly Format  | Description                                                         |
+-----------+------------------+---------------------------------------------------------------------+
| 2C <I1>   | brfalse.s target | branch to target if value is zero (false), short form               |
+-----------+------------------+---------------------------------------------------------------------+

Correspond à :

+-----------+------------------+-------------------------------------------------------------------------------------------+
| Format    | Assembly Format  | Description                                                                               |
+-----------+------------------+-------------------------------------------------------------------------------------------+
| 2D <I1>   | brtrue.s target  | branch to target if value is non-zero (true), short form                                  |
+-----------+------------------+-------------------------------------------------------------------------------------------+

Nous devrons donc remplacer le '2C' par un '2D'. De même que nous remplacions des '74' par des '75' pendant notre enfance :)

Rodriiiiiiiigueeeeeeeeeezeuh ! \o/

05.00.02 - OU MODIFIER LE CODE ?
--------------------------------

Encore faut-il le trouver notre '2C'...
Tout ce que nous savons c'est qu'il se situe en 'L_0095'. Cette adresse est une offset (en héxadécimal) par rapport au début de la méthode.
Le saut se situe donc en 'DebutDeLaMethode+95h'.

Trouvons donc l'adresse du debut de la méthode...

Pour ce faire nous allons utiliser le tool CFF Explorer de Ntoskrnl (disponible dans la section Liens sur le site de la NAS).

Lançons CFF Explorer et chargeons le crackme. Et rendons nous dans la section 'MetaData Tables', nous voyons apparaitre une liste contenant differents membres, dont le membre 'Method (6)'.

(Je ne parlerai pas des MetaDatas ni de la structure du format .NET, reportez-vous à la documentation de Microsoft "http://msdn.microsoft.com/net/ecma/default.asp", ou mieux, au tutoriel (The .NET File Format) écrit par Ntoskrnl "http://pmode.net/USERS/116/Files/dotnetformat.htm" toutefois ce n'est pas necessaire à la comprehension de la suite du tutoriel).

Nous allons donc eplucher chaques membres de la liste 'Method' à la recherche de notre 'button1_Click'. Il s'agit de la méthode numéro '5'.

Voici les informations que nous obtenons :

+-----------+----------+-------+----------+-------------------+
| Member    | Offset   | Size  | Value    | Meaning           |
+-----------+----------+-------+----------+-------------------+
| RVA       | 000033C0 | Dword | 00002370 |                   |
| ImplFlags | 000033C4 | Word  | 0000     |                   |
| Flags     | 000033C6 | Word  | 0081     |                   |
| Name      | 000033C8 | Word  | 0109     | button1_Click     |
| Signature | 000033CA | Word  | 0027     | Blob Index        |
| ParamList | 000033CC | Word  | 0002     | Param Table Index |
+-----------+----------+-------+----------+-------------------+

La seule chose qui nous interesse est le membre 'RVA' (Relative Virtual Adress), il nous permettra de trouver le debut de la méthode dans le fichier.

Pour expliquer ce qu'est une RVA je cite un extrait d'un tutoriel de Gamera qui devrait sortir prochainement :)

IMAGE BASE : C’est l’adresse à laquelle un fichier est chargé en mémoire.
RVA : Relative Virtual Address. Ce sont des références à des adresses en mémoire.
VA : C’est l’adresse de quelque chose en mémoire.
Ces 3 notions ne sont pas forcément évidentes à assimiler, mais elles restent néanmoins indispensables pour la suite. En fait, on peut dire qu’une RVA indique une distance à partir de l’image_base.
Voici un exemple imagé qui sera plus parlant :
Prenons un livre. Considérons que ce livre soit un programme. Vous décidez  d’ouvrir le livre (on va dire qu’ouvrir la couverture de ce livre est semblable au lancement d’un programme, il est donc chargé en mémoire) : Le début de ce livre est  l’image_base du programme. Dans ce livre, il y a un index, et chaque élément important  est référencé dans cet index. Quand nous cherchons un élément précis nous allons voir notre index, et ensuite nous nous rendons à la page indiquée par l’index. Eh bien la ligne qui nous dit à quelle page est l’élément qu nous cherchons est l’équivalent d’une RVA. Et la page en question, c’est la VA. Ensuite, nous allons à la page indiquée. Pour un programme, c’est pareil : on additionne l’image_base à la RVA, et nous trouvons l’adresse de l’élément voulu.

D’où: VA = RVA + IMAGE BASE

Prenons on exemple concret : l’image base du programme test.exe est 401000h.
La première section de ce programme a pour RVA 1000h.
Donc, la première section sera chargée à l’adresse 401000h + 1000h, soit 402000h.

Le hic c'est que la RVA désigne un endroit dans le programme chargé en mémoire. Nous, nous voulons l'endroit correspondant dans le fichier sur le disque dur...
CFF Explorer comporte un petit convertisseur qui nous sera bien utile : Address Converter.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

VA          : 00402370
RVA         : 00002370
File Offset : 00001370

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Notre offset est donc 1370h.

Donc si on récapitule depuis le début :

DebutDeLaMethode+95h = 1370h+95h = 1405h
On veut remplacer un '2C' par un '2D'.

Or, avec un éditeur héxadécimal, on se rend compte qu'en 1405h, il n'y à pas de trace de '2C'...

Le problème vient du Method Header. C'est un en-tête se situant au début de chaques méthodes, et qui contient quelques informations sur celles-ci. Du coup il faut prendre en compte la taille du header et l'additionner à notre offset (1405h) pour obtenir l'endroit exact où se situe notre opcode.

Pour faire plus simple :

DebutDeLaMethode+TailleDuHeader+95h = 1370h+XXXX+095h = YYYY

Le document 'The .NET File Format' nous dis à peu près les choses suivantes concernant le Method Header (je simplifie pour ne pas avoir a vous embrouiller d'avantage pour rien).

Il y à deux types de headers, le Tiny Header et le Fat Header. Le Tiny Header à une taille d'un octet tandis que le Fat Header, une taille de douze octets (0Ch).

Pour savoir quel type de header notre section comporte, il y à un flag au debut de celle-ci. Voici les valeurs nous permettant de determiner de quel header nous avons :

+------------------------+-------+------------------------+
| Flag                   | Value | Description            |
+------------------------+-------+------------------------+
| CorILMethod_FatFormat  | 0x3   | Method header is fat.  |
| CorILMethod_TinyFormat | 0x2   | Method header is tiny. |
+------------------------+-------+------------------------+

Ce flag se situe à la partie basse du premier octet de la méthode, donc dans notre cas en '1370h' (il s'agit de l'offset, hein). Un petit passage à l'éditeur héxadécimal :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

1370h : 13 30 03 00 AF 00 00 00 02 00 00 11 16 0D 02 7B

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

13 = 0x1 + 0x3 nous avons donc un Fat Header (de taille 0Ch donc).

Notre octet se trouve finalement en :

DebutDeLaMethode+TailleDuHeader+95h = 1370h+0Ch+095h = 1411h

Hop, édition héxadécimale :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

1410h : 0A 2C 0C 72 23 01 00 70 28 2B 00 00 0A 26 2A 72

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Et voila ! On remplace le '2C' (brfalse) par notre '2D' (brtrue) et c'est CrAxX0r3d !

Attention toutesfois, quand vous testerez vos exploit, à vérifier que vous avez bien entré un nom et un serial supérieurs à cinq charactères...

Good, good, good, goodboy ! You make me feel so gooooooooooooooooooood !
You know you make me feel so good.
You know you make me feel so good.

05.00.03 - J'AI RIEN COMPRIS ! ON RECOMMENCE
--------------------------------------------

Oui j'avoue que le fait d'aller à droite à gauche pour reverser un simple saut conditionnel peut être déroutant. Donc je résume ce que nous venons de faire :

Avec le décompileur :

- On situe notre saut à reverser dans le listing C# : : "if (blablabla != trucmuche)"
- On trouve la correspondance dans le listing IL : "brfalse.s"
- On note l'offset du saut conditionnel : "L_0095:"

Avec la documentation IL :

- On sait que notre saut à pour opcode '2C' et que le saut inverse à pour opcode '2D'.

Avec l'éditeur PE :

- On note la RVA de la méthode contenant notre code : "00002370h"
- On convertie la RVA en Offset : "00002370h -> 00001370h"

Avec l'éditeur héxadécimal :

- On va en 00001370h pour determiner le type de Method Header : "0x3 = Fat Header -> TailleDuHeader = 0Ch"
- On va à l'offset suivante : DebutDeLaMethode+TailleDuHeader+Offseth = 00001370h+0Ch+95h = 1411h.
- On remplace le '2C' par le '2D'.

Cric crac, l'affaire est dans le sac.

Vous trouvez sans doute cela un peu fastidueux, et c'est pas tout à fait faux. Cela-dit si les outils que nous nous servons pour cracker du .NET effectueraient cette tache à notre place, cela serait un peu plus simple. Comme voler la sucette de la bouche d'un enfant ou changer un 'je' en 'jne' ;)

Oh et puis faut pas vous plaindre, au moins on à vu du paysage :)

05.01 - RECOMPILATION
---------------------

Voici une technique je ne n'apprecie pas particulierement, mais ça vaut le coup d'en parler... Ne serait-ce qu'au moins une fois.

Il s'agit ici d'obtenir un version compilable du code fouri par un désILeur. Nous re-codons ainsi le programme avec un bête editeur de texte, et re-compilons le tout.

Pour obtenir un code compilable par le suite nous aurons besoin de IL DASM (disponible dans le SDK .NET Framework : "http://www.microsoft.com/downloads/search.aspx?displaylang=en&categoryid=10")

Rien de plus simple, on lance ildasm.exe (C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin), on charge le crackme dans le désILeur et on click sur "File|Dump".

Il n'y à rien a modifier aux options, celles par defaut convienent.

Nous obtenons donc un fichier '*.il' (je l'ai appelé 'DuMp.il' car je suis un hellith). Ainsi que deux fichiers '
WindowsApplication1.Form1.resources' et 'DuMp.res'.
Ces fichiers seront necessaire pour la re-compilation, et doivent donc toujours être dans le même repertoire que notre fichier '*.il'.

Ouvrons notre fichier avec Notepad, et regardons la ligne suivante :

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    IL_0095:  brfalse.s  IL_00a3

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

A moi aussi ça me rappel quelque chose... On change "brfalse.s" par "brtrue.s" et on sauvegarde le fichier.

Ensuite il nous reste plus qu'à compiler le fichier avec la ligne de commande suivante :

ilasm DuMp.il

Le programme 'ilasm.exe' se trouve dans le repertoire "C:\WINDOWS\Microsoft.NET\Framework\vX.X.XXXXX\".

Nous obtenons ainsi un fichier "DuMp.exe". On le test et... Oh ! Oui... C'est cracké :|

Comme dirait l'autre, c'est la porte ouverte à toutes les fenêtres...

 

 

06 - HAPPY END
==============

Nous pouvons conclure ce tutoriel sur ces quelques mots...

(cf. "http://msdn.microsoft.com/library/fre/default.asp?url=/library/FRE/dncscol/html/csharp01182001.asp").

(...)

Cela nous amène à une autre question sensible : « S'il est possible à tout le monde de voir mon IL et mes métadonnées, est-il possible de reconstituer la logique de mon code ? » La réponse est « Oui, cela est possible ». La situation n'est guère différente de ce qui se passait jusqu'à maintenant, puisque du code x86 optimisé pouvait également faire l'objet d'une reconstitution logique.

La différence réside dans l'effort nécessaire ; il est beaucoup plus facile de le faire dans .NET qu'avec du code x86 natif. Ceci est un autre problème sur lequel nous travaillons actuellement.

(...)

Vive le progrès, et vive la mariée :)

06.00 - PUSH REALLIFE/RET
-------------------------

Ainsi s'achève ce tutoriel qui m'a pris 5 jours à écrire pour ce crackme qui m'a pris 5 minutes à torcher :p

J'espère qu'il servira à quelqu'un, ou au pire qu'il m'apportera la gloire, l'argent et les femmes (faites un effort) :/

Salut à toi Neitsa (Peering Inside Les Cartons) pour son crackme qui m'a permis de me pencher sur le sujet.
Salut à toi Ntoskrnl pour son tutoriel sur le format .NET et son tool CFF Explorer.
Salut à toi NAS, FRET, ForumCrack, FFF, NGEN.
Salut à toi #fret, #uct, #nas (oui enfin il y à pas grand monde :D), #xXx (Pandemonium à un chan IRC...).
Salut à toi jeune lect(eur/rice) pour avoir lu ce tutoriel jusqu'au bout (sous les pavets, le savoir).
Salut à toi chienne de vie.
Salut à toi les autres...

Bisoux visqueux.
++Meat^NAS^FRET

   ^
. / \ .
 /___\
   .

"Hey nigga, why you fuck with me ?!"
"Fuck you nigga ! Cuz I can !"

- Dr Dre & Ice Cube - Natural Born Killaz